/*
 * Copyright (C) Francisco Morero Peyrona. All rights reserved.
 *
 * This software is published under the terms of Open Source
 * License version 1.1, a copy of which has been included with this
 * distribution in the License.txt file.
 */

package jdbcmanager.swing.text;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;

import jdbcmanager.swing.*;

/**
 * A JTextPane with added support for Undo / Redo actions.
 *
 * @author Francisco Morero Peyrona
 */

public class JxTextPane extends JTextPane
{
   public static final String undoActionName      = "Undo"; 
   public static final String redoActionName      = "Redo";
   public static final String selectAllActionName = "Select All";
   
   /** A reference to the internal <code>CutAction</code> class instance */
   protected CutAction       cutAction       = new CutAction();
   /** A reference to the internal <code>CopyAction</code> class instance */
   protected CopyAction      copyAction      = new CopyAction();
   /** A reference to the internal <code>PasteAction</code> class instance */
   protected PasteAction     pasteAction     = new PasteAction();
   /** A reference to the internal <code>UndoAction</code> class instance */
   protected UndoAction      undoAction      = new UndoAction();
   /** A reference to the internal <code>RedoAction</code> class instance */
   protected RedoAction      redoAction      = new RedoAction();
   /** A reference to the internal <code>SelectAllAction</code> class instance */
   protected SelectAllAction selectAllAction = new SelectAllAction();
   
   /** A reference to the popup shown when "right-click" */ 
   protected JPopupMenu   popupMenu    = new JPopupMenu();
   
   /** Should color change events be added to the UndoManager queue? */ 
   protected boolean      bAddStyleChange2Undo = true; 

   private UndoManager  undoManager  = new UndoManager();
   private UndoListener undoListener = new UndoListener();
   
   //---------------------------------------------------------//
   protected Action[] aactEdit = new Action[6];
   // Indexes in aactEdit array
   protected final static int nCUT       = 0;
   protected final static int nCOPY      = 1;
   protected final static int nPASTE     = 2;
   protected final static int nUNDO      = 3;
   protected final static int nREDO      = 4;
   protected final static int nSELECTALL = 5;

   //-------------------------------------------------------------------------//

   /**
    * Class constructor.
    */
   public JxTextPane()
   {      
      init();
   }

   /**
    * Redefined
    */
   public void setDocument( Document doc )
   {
      super.setDocument( doc );
      
      getDocument().addUndoableEditListener( this.undoListener );
   }

   /**
    * 
    * @param sText
    */
   public void setText( String sText )
   {
      super.setText( (sText == null) ? "" : sText.trim() );
      
      if( isEditable() )
      	setCaretPosition( getDocument().getLength() );
   }

   /**
    * 
    * @param bEnabled
    */
   public void setEnabled( boolean bEnabled )
   {
      super.setEnabled( bEnabled );
      updateActionsState( null );
   }

   /**
    * Performs a undo action.
    * <p>
    * The method <code>Action.actionPeformed()</code> calls here only if <code>Action.isEnabled() == true</code>, 
    * therefore calling this method directly performs the undo unconditionally.
    */
   public void undo()
   {
      if( this.undoAction.isEnabled() )
      {
         try
         {
            this.undoManager.undo();
         }
         catch( CannotUndoException exc )
         {
         }
      }
   }

   /**
    * Performs a redo action.
    * <p>
    * The method <code>Action.actionPeformed()</code> calls here only if <code>Action.isEnabled() == true</code>, 
    * therefore calling this method directly performs the undo unconditionally.
    */
   public void redo()
   {
      try
      {
         this.undoManager.redo();
      }
      catch( CannotRedoException exc )
      {
      }
   }
   
   /**
    * Clears all pending undo and redo.
    */
   public void clearUndoManager()
   {
      this.undoManager.discardAllEdits();
      updateActionsState( null );
   }
   
   /**
    * Redefined
    */
   public Action[] getActions()
   {
      Action[] aSuper  = super.getActions();
      Action[] aReturn = new Action[ aSuper.length + 2 ];
      
      System.arraycopy( aSuper, 0, aReturn, 0, aSuper.length );
      
      aReturn[ aSuper.length     ] = this.undoAction;
      aReturn[ aSuper.length + 1 ] = this.redoAction;
      
      // Replace DefaultEditorKit.<action> by the redefined actions created in this class
      aReturn[ getActionIndex( aReturn, DefaultEditorKit.cutAction       ) ] = this.cutAction;
      aReturn[ getActionIndex( aReturn, DefaultEditorKit.copyAction      ) ] = this.copyAction;
      aReturn[ getActionIndex( aReturn, DefaultEditorKit.pasteAction     ) ] = this.pasteAction;
      aReturn[ getActionIndex( aReturn, DefaultEditorKit.selectAllAction ) ] = this.selectAllAction;
      
      return aReturn;
   }
   
   /**
    * 
    * @param action
    */
   public void addActionKeyStroke( JAbstractAction action )
   {
      Keymap keymap = getKeymap( "JxTextPaneBindings" );
             keymap.addActionForKeyStroke( action.getKeyStroke(), action ); 
   }
 
   //-------------------------------------------------------------------------//

   private int getActionIndex( Action[] aAction, String sActionName )
   {
      for( int n = 0; n < aAction.length; n++ )
         if( aAction[n].getValue( Action.NAME ).equals( sActionName ) )
            return n;
            
      return -1;
   }
   
   /**
    * Enable and disable actions.
    * <p>
    * update is done by super. "Cut","Copy" and "Paste"  should be updated by super, 
    * but it aparently does not work properly.
    */
   private void updateActionsState( CaretEvent evt )
   {
      boolean bEditable = isEditable() && isEnabled();
      
      if( evt != null )
      {
         boolean bSelection = (evt.getDot() != evt.getMark());

         this.aactEdit[ nCUT  ].setEnabled( bSelection && bEditable );
         this.aactEdit[ nCOPY ].setEnabled( bSelection );
      }
      else
      {
         this.aactEdit[ nPASTE ].setEnabled( bEditable );
         this.aactEdit[ nSELECTALL ].setEnabled( getText().length() > 0 );
      
         this.undoAction.setEnabled( bEditable && this.undoManager.canUndo() );
         this.redoAction.setEnabled( bEditable && this.undoManager.canRedo() );
      }
   }

   private void init()
   {
      setMargin( new Insets( 3,3,3,3 ) );
      
      // Init undo manager --------------------------------
      getDocument().addUndoableEditListener( this.undoListener );
   
      // Creates keymap special keymap
      Keymap keymap = addKeymap( "JxTextPaneBindings", getKeymap() );
      setKeymap( keymap );
      
      // Updates keymap for undo actions
      addActionKeyStroke( this.undoAction );
      addActionKeyStroke( this.redoAction );

      // Init actions (innner classes in this class) ------
      this.aactEdit[ nCUT       ] = this.cutAction;
      this.aactEdit[ nCOPY      ] = this.copyAction;
      this.aactEdit[ nPASTE     ] = this.pasteAction;
      this.aactEdit[ nUNDO      ] = this.undoAction;
      this.aactEdit[ nREDO      ] = this.redoAction;
      this.aactEdit[ nSELECTALL ] = this.selectAllAction;
   
      // Init popup ---------------------------------------
      this.popupMenu.add( this.aactEdit[ nCUT   ] );
      this.popupMenu.add( this.aactEdit[ nCOPY  ] );
      this.popupMenu.add( this.aactEdit[ nPASTE ] );
      this.popupMenu.addSeparator();
      this.popupMenu.add( this.aactEdit[ nUNDO  ] );
      this.popupMenu.add( this.aactEdit[ nREDO  ] );
      this.popupMenu.addSeparator();
      this.popupMenu.add( this.aactEdit[ nSELECTALL ] );
      
      // Add listeners ------------------------------------
      addMouseListener( new MouseAdapter()
      {
         public void mousePressed( MouseEvent evt )
         {
            if( evt.isPopupTrigger() && popupMenu != null )
               popupMenu.show( evt.getComponent(), evt.getX(), evt.getY() );
         }
         
         public void mouseReleased( MouseEvent evt )
         {
            if( evt.isPopupTrigger() && popupMenu != null )
               popupMenu.show( evt.getComponent(), evt.getX(), evt.getY() );
         }
      } );
         
      addCaretListener( new CaretListener()
      {
         public void caretUpdate( CaretEvent evt )
         {
            updateActionsState( evt );
         }
      });
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: CutAction
   //-------------------------------------------------------------------------//
   private final class CutAction extends DefaultEditorKit.CutAction
   {
      private CutAction()
      {
         putValue( Action.SHORT_DESCRIPTION, "Cut" );
         putValue( Action.LONG_DESCRIPTION , "Cut" );
         putValue( Action.SMALL_ICON     , new ImageIcon( getClass().getResource( "images/cut.gif" ) ) );
         putValue( Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_X, Event.CTRL_MASK ) );   
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         super.actionPerformed( evt );
         requestFocus();
      } 
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: CopyAction
   //-------------------------------------------------------------------------//
   private final class CopyAction extends DefaultEditorKit.CopyAction
   {
      private CopyAction()
      {
         putValue( Action.SHORT_DESCRIPTION, "Copy" );
         putValue( Action.LONG_DESCRIPTION , "Copy" );
         putValue( Action.SMALL_ICON       , new ImageIcon( getClass().getResource( "images/copy.gif" ) ) );
         putValue( Action.ACCELERATOR_KEY  , KeyStroke.getKeyStroke( KeyEvent.VK_C, Event.CTRL_MASK ) );
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         super.actionPerformed( evt );
         requestFocus();
      } 
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: PasteAction
   //-------------------------------------------------------------------------//
   private final class PasteAction extends DefaultEditorKit.PasteAction
   {
      private PasteAction()
      {
         putValue( Action.SHORT_DESCRIPTION, "Paste" );
         putValue( Action.LONG_DESCRIPTION , "Paste" );
         putValue( Action.SMALL_ICON     , new ImageIcon( getClass().getResource( "images/paste.gif" ) ) );
         putValue( Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_V, Event.CTRL_MASK ) );
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         super.actionPerformed( evt );
         requestFocus();
      } 
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: SelectAllAction
   //-------------------------------------------------------------------------//
   private final class SelectAllAction extends JAbstractAction
   {
      private SelectAllAction()
      {
         setName( selectAllActionName );
         setActionCommandKey( "SELECTALL" );
         setShortDescription( selectAllActionName );
         setLongDescription( "Select (highlights) all text in editor" );
         setSmallIcon( new ImageIcon( getClass().getResource( "images/select_all.gif" ) ) );
         setMnemonicKey( 'S' );
         setKeyStroke( KeyStroke.getKeyStroke( KeyEvent.VK_A, Event.CTRL_MASK ) );
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         selectAll();
         requestFocus();
      } 
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: UndoAction
   //-------------------------------------------------------------------------//
   private final class UndoAction extends JAbstractAction
   {
      private UndoAction()
      {
         setName( undoActionName );
         setActionCommandKey( "UNDO" );
         setShortDescription( undoActionName );
         setLongDescription( "Undo last action" );
         setSmallIcon( new ImageIcon( getClass().getResource( "images/undo.gif" ) ) );
         setMnemonicKey( 'u' );
         setKeyStroke( KeyStroke.getKeyStroke( KeyEvent.VK_Z, Event.CTRL_MASK ) );
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         if( isEnabled() )
         {
            super.actionPerformed( evt );
            undo(); 
            requestFocus();
            updateActionsState( null );
         }
      }
   }

   //-------------------------------------------------------------------------//
   // INNER CLASS: RedoAction
   //-------------------------------------------------------------------------//
   private final class RedoAction extends JAbstractAction
   {
      private RedoAction()
      {
         setName( redoActionName );
         setActionCommandKey( "REDO" );
         setShortDescription( redoActionName );
         setLongDescription( "Redo last undoed action" );
         setSmallIcon( new ImageIcon( getClass().getResource( "images/redo.gif" ) ) );
         setMnemonicKey( 'r' );
         setKeyStroke( KeyStroke.getKeyStroke( KeyEvent.VK_Y, Event.CTRL_MASK ) );
      }
      
      public void actionPerformed( ActionEvent evt )
      {
         if( isEnabled() )
         {
            super.actionPerformed( evt );
            redo();
            requestFocus();
            updateActionsState( null );
         }
      }
   }
   
   //-------------------------------------------------------------------------//
   // INNER CLASS: UndoableEditListener
   //-------------------------------------------------------------------------//
   private class UndoListener implements UndoableEditListener
   {
      public void undoableEditHappened( UndoableEditEvent evt )
      { 
         AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) evt.getEdit();
         
         if( JxTextPane.this.bAddStyleChange2Undo || event.getType() != DocumentEvent.EventType.CHANGE )
         {
            JxTextPane.this.undoManager.addEdit( evt.getEdit() );
            JxTextPane.this.updateActionsState( null );
         }
      }
   }
}
